namespace ScrumSprintMonitor.Modules.WorkItemMonitor.ViewModels
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.Collections.ObjectModel;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Linq;
	using System.Windows;

	using Infrastructure;
	using Infrastructure.Configuration;

	using Microsoft.Practices.ServiceLocation;

	using UI.Wpf;
	using UI.Wpf.ViewModels;

	#endregion

	public class SprintBurndownChartViewModel : SprintViewModelBase,
	                                            ISprintBurndownChartViewModel
	{
		static SprintBurndownChartViewModel()
		{
			var dependsOn = Register<SprintBurndownChartViewModel>();

			dependsOn.Call(me => me.AttachToBurndownDataPoints()).OnChanged(me => me.CurrentSprint.BurndownDataPoints);
			dependsOn.Call(me => me.OnPropertyChanged("BurndownChartStartDate")).OnChanged(me => me.CurrentSprint.Start);
			dependsOn.Call(me => me.OnPropertyChanged("BurndownChartEndDate")).OnChanged(me => me.CurrentSprint.End);
			dependsOn.Call(me => me.OnPropertyChanged("IsBuildingBurndownDataPoints")).OnChanged(me => me.CurrentSprint.IsBuildingBurndownDataPoints);
			dependsOn.Call(me => me.RecalculateBurndownChartTrendLine()).OnChanged(me => me.IsBuildingBurndownDataPoints);
		}

		public SprintBurndownChartViewModel(IServiceLocator serviceLocator, IShellLayoutConfiguration shellLayoutConfiguration)
			: base(serviceLocator, false)
		{
			this.shellLayoutConfiguration = shellLayoutConfiguration;

			for (int index = 0; index < 8; ++index)
			{
				this.teamNamesList.Add(null);
				this.burndownDataPointsList.Add(new ObservableCollection<BurndownDataPoint>());
			}
		}

		#region Private Fields

		#region Burndown Chart

		private readonly ObservableCollection<BurndownDataPoint> burndownAggregateDataPointList = new ObservableCollection<BurndownDataPoint>();

		private readonly ObservableCollection<ObservableCollection<BurndownDataPoint>> burndownDataPointsList =
			new ObservableCollection<ObservableCollection<BurndownDataPoint>>();

		private readonly ObservableCollection<BurndownDataPoint> burndownTrendLineDataPointList = new ObservableCollection<BurndownDataPoint>();
		private readonly ObservableCollection<string> teamNamesList = new ObservableCollection<string>();
		private BindingList<BurndownDataPoint> currentSprintBurndownDataPoints;

		#endregion

		private readonly IShellLayoutConfiguration shellLayoutConfiguration;

		#endregion

		#region Implementation of ISprintBurndownChartViewModel

		public Visibility BurndownChartVisibility
		{
			[DebuggerStepThrough]
			get { return (this.shellLayoutConfiguration.ShowBurndownChart ? Visibility.Visible : Visibility.Collapsed); }
		}

		public bool IsBuildingBurndownDataPoints
		{
			[DebuggerStepThrough]
			get { return (CurrentSprint != null ? CurrentSprint.IsBuildingBurndownDataPoints : false); }
		}

		public DateTime BurndownChartStartDate
		{
			[DebuggerStepThrough]
			get { return (CurrentSprint != null ? CurrentSprint.Start.Date : DateTime.MinValue); }
		}

		public DateTime BurndownChartEndDate
		{
			[DebuggerStepThrough]
			get { return (CurrentSprint != null ? CurrentSprint.End : DateTime.MinValue); }
		}

		public ObservableCollection<string> TeamNames
		{
			[DebuggerStepThrough]
			get { return this.teamNamesList; }
		}

		public ObservableCollection<BurndownDataPoint> BurndownTrendLineDataPoints
		{
			[DebuggerStepThrough]
			get { return this.burndownTrendLineDataPointList; }
		}

		public ObservableCollection<BurndownDataPoint> BurndownAggregateDataPoints
		{
			[DebuggerStepThrough]
			get { return this.burndownAggregateDataPointList; }
		}

		public ObservableCollection<ObservableCollection<BurndownDataPoint>> BurndownDataPointsForTeam
		{
			[DebuggerStepThrough]
			get { return this.burndownDataPointsList; }
		}

		#endregion

		#region Private Methods

		private BindingList<BurndownDataPoint> CurrentSprintBurndownDataPoints
		{
			[DebuggerStepThrough]
			get { return this.currentSprintBurndownDataPoints; }
			set
			{
				if (value == CurrentSprintBurndownDataPoints)
				{
					return;
				}

				foreach (var burndownDataPoints in this.burndownDataPointsList)
				{
					burndownDataPoints.Clear();
				}
				if (this.currentSprintBurndownDataPoints != null)
				{
					this.currentSprintBurndownDataPoints.ListChanged -= OnBurndownDataPointsListChanged;
				}

				this.currentSprintBurndownDataPoints = value;

				if (this.currentSprintBurndownDataPoints != null)
				{
					InitializeTeamBurndownListsFromSprintData();

					this.currentSprintBurndownDataPoints.ListChanged += OnBurndownDataPointsListChanged;
				}

				RecalculateBurndownDependents();
				OnPropertyChanged("CurrentSprintBurndownDataPoints");
			}
		}

		private void InitializeTeamBurndownListsFromSprintData()
		{
			var teams = this.currentSprintBurndownDataPoints.AsParallel().AsUnordered().Select(x => x.Team).Distinct().ToArray();

			foreach (string team in teams)
			{
				string s = team;
				var teamDataPoints = this.currentSprintBurndownDataPoints.Where(p => p.Team == s);
				var viewModelDataPoints = GetBurndownDataPointsForTeam(team);
				foreach (var dataPoint in teamDataPoints)
				{
					viewModelDataPoints.Add(dataPoint);
				}
			}
		}

		private ObservableCollection<BurndownDataPoint> GetBurndownDataPointsForTeam(string team)
		{
			int index = this.teamNamesList.IndexOf(team);
			if (index < 0)
			{
				index = this.teamNamesList.IndexOf(this.teamNamesList.FirstOrDefault(x => x == null));
				if (index < 0)
				{
					index = this.teamNamesList.Count;
					this.teamNamesList.Add(team);
				}
				else
				{
					this.teamNamesList[index] = team;
				}
			}

			while (index >= this.burndownDataPointsList.Count)
			{
				this.burndownDataPointsList.Add(new ObservableCollection<BurndownDataPoint>());
			}

			return this.burndownDataPointsList[index];
		}

		private void RecalculateBurndownDependents()
		{
			RecalculateBurndownChartTrendLine();
			RecalculateBurndownAggregate();
		}

		private void AttachToBurndownDataPoints()
		{
			CurrentSprintBurndownDataPoints = (CurrentSprint != null ? CurrentSprint.BurndownDataPoints : null);

			RecalculateBurndownDependents();
		}

		private void RecalculateBurndownAggregate()
		{
			if (CurrentSprintBurndownDataPoints == null || !CurrentSprintBurndownDataPoints.Any())
			{
				this.burndownAggregateDataPointList.Clear();
				return;
			}

			var dataPointsByDate = CurrentSprintBurndownDataPoints.GroupBy(x => x.Time);
			foreach (var dataPointsInTimeStamp in dataPointsByDate)
			{
				var time = dataPointsInTimeStamp.Key;
				double workDone = dataPointsInTimeStamp.Sum(x => x.WorkDone);
				double workRemaining = dataPointsInTimeStamp.Sum(x => x.WorkRemaining);

				var aggregateDataPoint = this.burndownAggregateDataPointList.FirstOrDefault(x => x.Time == time);
				if (aggregateDataPoint == null)
				{
					aggregateDataPoint = new BurndownDataPoint
					                     	{
					                     		Time = time
					                     	};
					this.burndownAggregateDataPointList.Add(aggregateDataPoint);
				}
				aggregateDataPoint.WorkDone = workDone + workRemaining;
				aggregateDataPoint.WorkRemaining = workRemaining;
			}
		}

		private void RecalculateBurndownChartTrendLine()
		{
			this.burndownTrendLineDataPointList.Clear();

			if (CurrentSprintBurndownDataPoints != null && CurrentSprintBurndownDataPoints.Count >= 2)
			{
				if (IsBuildingBurndownDataPoints)
				{
					return;
				}

				var minimumDate = CurrentSprint.Start;
				var dataPointsByDate = CurrentSprintBurndownDataPoints.GroupBy(x => x.Time);
				var xValues = new List<double>(32);
				var yValues = new List<double>(32);
				foreach (var dataPointsInTimeStamp in dataPointsByDate)
				{
					double workRemaining = dataPointsInTimeStamp.Sum(x => x.WorkRemaining);

					xValues.Add((dataPointsInTimeStamp.Key - minimumDate).TotalDays);
					yValues.Add(workRemaining);
				}

				if (xValues.Count >= 2)
				{
					var trendLine = new Trendline(yValues, xValues);
					var trendLineEnd = BurndownChartEndDate;
					this.burndownTrendLineDataPointList.Add(new BurndownDataPoint
					                                        	{
					                                        		Time = minimumDate,
					                                        		WorkRemaining = trendLine.Start
					                                        	});
					this.burndownTrendLineDataPointList.Add(new BurndownDataPoint
					                                        	{
					                                        		Time = trendLineEnd,
					                                        		WorkRemaining = trendLine.CalculateEnd((trendLineEnd - minimumDate).TotalDays)
					                                        	});
				}
			}
		}

		private void OnBurndownDataPointsListChanged(object sender, ListChangedEventArgs e)
		{
			switch (e.ListChangedType)
			{
				case ListChangedType.ItemAdded:
				{
					var dataPoint = CurrentSprintBurndownDataPoints[e.NewIndex];
					var viewModelDataPoints = GetBurndownDataPointsForTeam(dataPoint.Team);
					viewModelDataPoints.Add(dataPoint);

					RecalculateBurndownDependents();
					break;
				}
				case ListChangedType.ItemChanged:
					RecalculateBurndownDependents();
					break;
				default:
					throw new NotImplementedException();
			}
		}

		#endregion

		#region Overrides

		protected override void Dispose(bool disposing)
		{
			if (disposing)
			{
				CurrentSprintBurndownDataPoints = null;
			}

			base.Dispose(disposing);
		}

		#endregion
	}
}